public class Main {
    public static void main(String[] args) {
        // **** Thread的常见构造方法****
        // 方法                                //说明
        //Thread()                          //创建线程对象
        // Thread(Runnable target)         //使用 Runnable 对象创建线程对象
        //Thread(String name)               //创建线程对象，并命名
        // Thread(Runnable target, String name)        //使用 Runnable 对象创建线程对象，并命名
        // 【了解】Thread(ThreadGroup group,Runnable target)  //线程可以被用来分组管理，分好的组即为线程组，这
        //        //个目前我们了解即可



        // **** 2.2 Thread 的几个常见属性 ****
        // 属性                   //获取方法
        //ID                     //getId()
        //名称                   // getName()
        //状态                   //getState()
        //优先级                 // getPriority()
        //是否后台线程            //isDaemon()
        //是否存活              // isAlive()
        //是否被中断           //isInterrupted()

        // ID 是线程的唯一标识，不同线程不会重复
        //名称是各种调试工具用到
        //状态表示线程当前所处的一个情况，下面我们会进一步说明
        //优先级高的线程理论上来说更容易被调度到
        //关于后台线程，需要记住一点：JVM会在一个进程的所有非后台线程结束后，才会结束运行。
        //是否存活，即简单的理解，为 run 方法是否运行结束了
        //线程的中断问题，下面我们进一步说明

        // 有前台进程和后台进程
        // 前台线程会阻止进程结束，前台线程的工作没做完进程是完不了的
        // 后台线程，不会阻止进程结束，后台线程没有工作完，进程也是可以结束的
        // 代码里手动创建的线程，默认都是前台的
        // 其他的jvm里自带的线程都是后台的

        //也可以手动使用setDaemon设置成的后台线程
        // 当前线程的前后台 只是取决于你调用调的setDaemon这个方法干了什么

        //***** 2.3 中断一个线程 *****
        // 中断的意思是 不是让线程立刻停止 而是通知线程，你应该要停止。是否真的停止，取决于线程这里具体的代码

        // 有两种实现方式
        // 1. 使用标志位来控制线程是否停止
        // 2.使用Thread 自带的标志位来进行判定（这个东西是可以唤醒上面的sleep这样的方法）currentThread()
        // currentThread() 通过这个方法可以获取到这个线程
        // 换句话说就是 哪个线程调用了这个方法，就是得到了哪个线程的引用 很类似于this

        //示例-2: 使用Thread.interrupted() 或者 义标志位.Thread.currentThread().isInterrupted() 代替自定Thread
        // 内部包含了一个 boolean 类型的变量作为线程是否被中断的标记.

        // thread 收到通知的方式有两种：
        //1. 如果线程因为调用 wait/join/sleep 等方法而阻塞挂起，则以 InterruptedException 异常的形式通
        //知，清除中断标志
        //当出现 InterruptedException 的时候, 要不要结束线程取决于 catch 中代码的写法. 可以选择
        //忽略这个异常, 也可以跳出循环结束线程.
        // 2. 否则，只是内部的一个中断标志被设置，thread 可以通过
        //Thread.interrupted() 判断当前线程的中断标志被设置，清除中断标志
        //Thread.currentThread().isInterrupted() 判断指定线程的中断标志被设置，不清除中断标志
        //这种方式通知收到的更及时，即使线程正在 sleep 也可以马上收到。


        // 示例-3 观察标志位是否清除
        // 标志位是否清除, 就类似于一个开关.
        //Thread.isInterrupted() 相当于按下开关, 开关自动弹起来了. 这个称为 "清除标志位"
        //Thread.currentThread().isInterrupted() 相当于按下开关之后, 开关弹不起来, 这个称为
        // "不清除标志位".
        //使用Thread.isInterrupted() , 线程中断会清除标志位.
        //使用Thread.currentThread().isInterrupted() , 线程中断标记位不会清除.


        //**** 2.5 等待一个线程 ****
        // join，等待一个线程是指等待一个线程的中断
        // 做的事情就是 控制两个线程的结束顺序.

        // **** 2.6 获取当前线程引用 ****
        // public static Thread currentThread();  返回当前线程对象的引用

        //****  2.7 休眠当前线程 ****
        // ，有一点要记得，因为线程的调度是不可控的，所以，这个方法只能保证实际休眠时间是大于等于参数设置的休眠时间的。
        // （PCB的阻塞状态）（挂起（hung up）和阻塞（block）是一个意思 只不过是不同术语）
        // public static void sleep(long millis) throws InterruptedException 休眠当前线程 millis
        //毫秒

        // public static void sleep(long millis, int nanos) throws
        // InterruptedException 可以更高精度的休眠

        // PCB 是用链表来组织的 （实际的情况并不是一个简单的链表 ，而是一系列由链表组织的）


        // ***** 3.线程的状态 ******
        // 3.1 观察线程的所有状态
        //线程的状态是一个枚举类型 Thread.State
        for(Thread.State state : Thread.State.values()){
            System.out.println(state);
        }
        // 运行结果共有有 一下几种
        //NEW: 安排了工作, 还未开始行动。（创建了Thread对象，但是还没有调用start（内核里面还没有创建对应的PCB））
        //RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. （可运行的 （表示正在CPU上执行 要么就在就绪队列里，随时可以去CPU上执行））
        //BLOCKED: 这几个都表示排队等着其他事情
        //WAITING: 这几个都表示排队等着其他事情       （这三个都是阻塞（都是表示PCB正在阻塞队列中 只不过这几个状态是不同原因的阻塞））
        //TIMED_WAITING: 这几个都表示排队等着其他事情
        //TERMINATED: 工作完成了.（表示内核中的PCB已经执行完毕了，但是Thread对象还在）

        // 3.2 线程状态和状态转移的意义 （重点）
        // 上图
        /*                                                                          NEW（线程对象刚创建对象，PCB还没有创建）
                                                                                            ↓ （调用 start 真正创建线程）
（TIME_WAITING等待一定的时间，时间到了自然就回到RUNNABLE状态）调用sleep就会使线程阻塞）   ←     RUNNABLE状态（PCB 已经有了，线程参与调度执行）
（WAITING等待其他人来通知）（条件满足，通知到了才会回到RUNNABLE状态）（调用wait/join就会使线程进入阻塞状态）←
 （BLOCKED 等待锁产生的阻塞）（只有获取到锁才会回到RUNNABLE状态）（由于加锁操作才进入到阻塞）←

                                                                                             ↓  （线程执行完run放啊，此时线程执行结束）
                                                                                    TERMINATED状态（PCB就释放了，但是对象还在）


         */



        // ***** 接下来写一个案例： 多线程的意义到底是啥?*****
        // 写个代码来深切的感受一下叭~~ 来看看单个线程和多个线程之间，执行速度上的差别
        // 程序、分成、CPU密集（包含了大量的加减乘除等算数运算）、IO密集（涉及到读写文件，读写控制台，读写网络）
        // 主要介绍CPU密集
        // 实现环境： 假设现在有一个运算量很大的任务.看一下多线程和单线程的区别.


        // ***** 4. 多线程带来的的风险-线程安全 (重点) *****
        // 是多线程编程中最难的地方也是一个最重要的地方，还是一个最容易出错的地方，更是一个特别爱考的地方，也是实际工作中最容易出错的地方

        // 万恶之源，罪魁祸首就是多线程的抢占式方式带来的随机性
        // 如果没有多线程，此时程序代码执行的顺序式固定的（只有一条路）。代码顺序固定，程序的结果就是固定的【单线程的情况下，只需要理清这一条路即可】
        // 如果有了多线程，此时抢占式执行下，代码执行的顺序是随机的，此时的可能性就从 一种情况 变成了 无数种

        // 所以就需要保证这无数种线程调度顺序的情况下，代码的执行结果都是正确的
        // 只要有一种情况下，代码结果不正确，就视为是有bug的，线程不安全

        // 所以能否消除这样的随机性？ 调度的源头是来自操作系统的内核的实现
        // 1.你改不了，2.即使你改了，别人也不一定买账

        // 代码实现ThreadDemo5


        // **** synchronized 的使用方法 *****
        // 1. 修饰方法
        //  1） 修饰普通方法  （进入方法就加锁，离开方法就解锁）
        //  2） 修饰静态方法   （作用同上）                         ----> 但是这两个操作加锁的”对象“不同
        // 2. 修饰代码块

        // 修饰普通方法，锁对象就是this  （把锁加到this对象上）
        // 修饰静态方法，锁对象就是类对象（Counter.class）
        // 修饰代码块，显式/手动指定锁对象
        //（一定要理解清楚当前的锁对象式哪一个）


        // 如果两个线程针对的是同一个对象加锁 会产生阻塞等待
        // 但如果两个线程针对的是不同的对象，就不会产生阻塞等待
        // 无论这个对象是啥样的对象，原则就是一条，锁对象相同，就会产生锁竞争（产生阻塞等待）
        // 锁对象不同就不会产生锁竞争（就不会产生阻塞等待）

        // ***** 5 synchronized 关键字-监视器锁monitor lock *****

        // **** 5.1 synchronized 的特性 *****
        // 1) 互斥
        //  进入 synchronized 修饰的代码块, 相当于 加锁
        //退出 synchronized 修饰的代码块, 相当于 解锁
        // synchronized用的锁是存在Java对象头里的。 可以粗略理解成, 每个对象在内存中存储的时候, 都存有一块内存表示当前的 "锁定" 状态(类似于厕
        //所的 "有人/无人").
        //如果当前是 "无人" 状态, 那么就可以使用, 使用时需要设为 "有人" 状态.
        //如果当前是 "有人" 状态, 那么其他人无法使用, 只能排队


        // **** synchronized的底层是使用操作系统的mutex lock实现的.  ****

        //2) **** 可重入 ****
        // synchronized 同步块对同一条线程来说是可重入的，不会出现自己把自己锁死的问题；
        // 一个线程针对同一个对象，连续加锁两次是否会有问题，如果没有问题就是 可重入，如果没有问题就是 不可重入的

        // 第一次加锁, 加锁成功
        //lock();
        // 第二次加锁, 锁已经被占用, 阻塞等待.
        //lock();

        // 所以为了避免不小心就死锁了，Java就把 synchronized 设定成可重入的
        // （如果加锁线程和持有线程是同一个，就直接放过，否则就阻塞）
        // （死锁有很多的情况）

        //    // **** 死锁另外的一种情况 ****
        // 死锁是一个非常影响程序的
        // 一旦程序出现死锁，就会导致线程崩溃 （无法继续执行后续工作了）程序势必会有严重bug
        // 而且死锁是非常隐蔽的。开发阶段，不经意间就会写出死锁的代码。而且死锁也是不容易测试出来的
        //  而且 死锁 是一个概率问题，并不是一定会 100% 的死锁，（有的时候出现死锁 有的时候不出现死锁）

        // **** 死锁典型的场景 *****
        // 1. 一个线程、一把锁，连续加锁两次。如果锁是不可重入锁（针对上述情况不会死锁） 就会死锁
        //  java 里的 synchronized 和 ReentrantLock 都是可重入锁，这个现象演示不了
        // 2. 两个线程两把锁 thread1 和 thread2 各自先针对 锁A 和 锁B 加锁，再尝试获取对方的锁。
        //  （在进行获取对方的锁呢就有可能出现互不相让的情况，这样也就出现了死锁）
        // 3.多个线程多把锁 （相当于 2 的一般情况，2 只是一个特殊情况）
        // （这就涉及到哲学家就餐问题）
        // （死锁的四个必要条件 ： 1.互斥使用  2.不可抢占  3.请求和保持 4.循环等待）
        // (只有第4种情况才是程序员可以控制的 ， 所以突破口就是 循环等待这个条件)
        // (方法 ： 给锁编号。然后指定一个固定的顺序（从小到大或者从大到小）来加锁)

        // 1.死锁是怎么回事 2. 死锁的三个典型情况 3.可重入和不可重入 4.死锁的四个必要条件 5.如何破除死锁

        // **** 5.3 Java 标准库中的线程安全类 ****
        // （如果多个线程执行同一个集合类，就要考虑到线程安全的事情了）
        // Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

        //ArrayList
        // LinkedList
        // HashMap
        // TreeMap                //  这些类在多线程代码中使用要格外注意 （有线程安全问题了就加锁，没有就不用管放心用）
        // HashSet
        // TreeSet
        // StringBuilder

        // 但是还有一些是线程安全的. 使用了一些锁机制来控制.

        // Vector (不推荐使用)
        // HashTable (不推荐使用)     // 已经内置了 synchronized 加锁。相对来说，更安全一点 (强行加锁)
        // ConcurrentHashMap （这个是可以推荐用的，在线程安全的问题当中）
        // StringBuffer (也不太推荐用)

        // 还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的
        // String

        // 加锁这个操作是有副作用的 （额外的时间开销）




        // **** 6. volatile 关键字 ****
        // volatile(会念也会拼写) 能保证内存可见性（和内存可见性问题密切相关）
        // ThreadVola 里面
        // 这个关键字呢 可以给每一个变量都加，也可以只加一部分...看看你的需求
        // 不能修饰方法里面的变量，因为方法里的局部变量（只能在当前方法里使用的，出了方法，变量就没了，也就是对当前线程可见，对其他线程就不可见），
        // 只能在你当前线程里面使用。不能多线程之间同时读取/修改
        // （如果一个变量在两个线程种需要一个读 一个写 就需要考虑volatile的问题了）

        // 上述所说的内存可见性 是编译器优化的问题。

        // 内存可见性问题，其他的一些资料i，体验到JMM (Java Memory Model （Java内存模型）)
        // 从 JMM 角度重新表示内存可见性问题
        // Java程序里，主内存（内存），每个线程还有自己的工作内存 （thread1 和 thread2 工作内存不是同一个东西），
        // thread1线程进行读取的时候，只读取了自己工作内存的值
        // thread2 线程进行修改的时候，先修改的 工作内存（工作存储区）（CPU寄存器） 的值，然后再把工作内存的内容同步到主内存中
        // 但是由于编译器优化，导致thread1没有重新的从内存中同步数据到内存中，读到的结果就是“修改之前”的结果
        // 工作内存不一定只是 cpu 的存储器，还可能是 缓存cache
        // 因为 寄存器存储空间小，读写速度快
        // 中间搞了一个cache，存储空间居中，读写速度居中
        // 内存存储空间大，读写速度慢
        // 当CPU 要读取一个内存数据的时候，可能是直接读内存，也可能是读cache还可能读寄存器...
        // 所以 工作内存（工作存储区）：CPU寄存器 + CPU的cache


        //  6.1 volatile 不保证原子性
        // volatile 和 synchronized 有着本质的区别. synchronized 能够保证原子性, volatile 保证的是内存可见
        //性.

        // 6.2 synchronized 也能保证内存可见性 （存疑还不确定）


        // 7 **** 7. wait 和 notify ****
        // 线程最大的问题就是抢占式执行和随机调度 , 而 我们写代码的时候不喜欢随机。喜欢确定的东西
        // 所以就设计了一些api 让线程主动阻塞，主动放弃cpu
        // 通过 wait 操作让另一个线程先进行阻塞的状态，等到线程干的差不多了 再通过 notify 通知呢个线程 ，并把它唤醒
        // 那么上述场景 使用join 或者 sleep 可以吗
        // 可以，但是效果不好，使用join则必须要等thread1线程执行完 thread2才能继续执行。但是如果让t1先干50%的话再让t2开始行动，join无能为力
        // 使用sleep 是指定一个休眠时间，但是thread1执行的这些活到底花了多长时间，不好估计
        // 使用wait和notify可以更好的解决上述问题
        // wait 和 notify 使用起来要比join麻烦不少....

        // 希望多个线程有先后执行顺序

        // 完成这个协调工作, 主要涉及到三个方法
        //wait() / wait(long timeout): 让当前线程进入等待状态.
        //notify() / notifyAll(): 唤醒在当前对象上等待的线程.
        // 注意:  wait, notify, notifyAll 都是 Object 类的方法.

        // **** 9. 多线程案例****
        // 9.1 单例模式
        // 我们介绍了两种模式 一种是 懒汉模式 , 一种是 饿汉模式
        // 那么这两种模式在 多线程当中哪一个是安全的呢
        // 饿汉模式 : 因为饿汉模式 这里 在多线程调用当中 只涉及到了"读操作"
        // 而懒汉模式: 在多线程调度当中 既有 读又有写
        // 所以 如何让懒汉模式成为线程安全呢
        // 那就是加锁,但是这个加锁又要加到哪里呢?
        // 看代码

        // 加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.
        //因此后续使用的时候, 不必再进行加锁了.
        //外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.
        //同时为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile .
        //当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,
        //其中竞争成功的线程, 再完成创建实例的操作.
        //当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
        //1) 有三个线程, 开始执行getInstance , 通过外层的
        // if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁
        //2) 其中线程1 率先获取到锁, 此时线程1 通过里层的  if (instance == null) 进一步确认实例是否已经创建. 如果没创建, 就把这个实例创建出来
        // 3) 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来
        //确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了
        // 4) 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从
        //而不再尝试获取锁了. 降低了开销


        // 9.2 阻塞式队列
        // 阻塞队列是什么
        //阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则.
        //阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
        //当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
        //当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
        //阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型.

        // 实际上还有一些特殊的队列,不一定非得最受先进先出的(优先级队列)
        // 还有一种队列是 消息队列 ,相当于是在阻塞队列的基础上,加上了个"消息类型"按照制定类别进行先进先出
        // "消息队列"的本身还是一个"数据结构"

        // 生产者消费者模型
        //生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
        //生产者和消费者彼此之间不直接通讯，而通过阻塞队列来进行通讯，所以生产者生产完数据之后不用等
        //待消费者处理，直接扔给阻塞队列，消费者不找生产者要数据，而是直接从阻塞队列里取.

        // 生产者和消费者的好处:
        //1) 阻塞队列就相当于一个缓冲区，平衡了生产者和消费者的处理能力.(可以做到"削峰填谷");
        //2) 阻塞队列也能使生产者和消费者之间 解耦("低耦合: 在两个模块之间 其中一个模块发生改变 另一个模块因他收到的影响很小,这就叫低耦合 反之就是高耦合").
        // (耦合可以理解成一种 关联程度 ) , 咱们写代码的时候 就是要追求"低耦合"

        // 所以 "解耦" 就是一种降低耦合的过程

        // 标准库中的阻塞队列
        //在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
        //BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
        //put 方法用于阻塞式的入队列, take 用于阻塞式的出队列.
        //BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.

        // 阻塞队列实现
        //通过 "循环队列" 的方式来实现.
        //使用 synchronized 进行加锁控制.
        //put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
        //定队列就不满了, 因为同时可能是唤醒了多个线程).
        //take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)


        // 9.3 定时器
        //定时器是什么
        //定时器也是软件开发中的一个重要组件. 类似于一个 "闹钟". 达到一个设定的时间之后, 就执行某个指定
        //好的代码.
        // 这里的定时器 不是提醒 而是执行一个实现准备好的代码

        // 定时器是一种实际开发中非常常用的组件.
        //比如网络通信中, 如果对方 500ms 内没有返回数据, 则断开连接尝试重连.
        //比如一个 Map, 希望里面的某个 key 在 3s 之后过期(自动删除).
        //类似于这样的场景就需要用到定时器

        // 9.4 线程池
        // 线程池是什么?
        // 使用线程池 来降低创建/销毁线程的开销
        // 事先把需要使用的线程创建好,放到"池"中.后面需要使用的时候直接取,如果用完了就还给池
        // 线程池最大的好处就是减少每次启动、销毁线程的损耗。
        // 创建/销毁线程 是由 操作系统内核完成的
        // 程序中的内核态 进行的操作都是在操作系统内核中完成的.内核会给程序提供一些api,称为系统调用.程序可以调用系统调用,驱使内核完成一些工作

        // 相比于 内核 来说,用户态 程序执行的行为是可控的
        // 但是 要通过内核 从系统这里创建个线程, 就需要通过系统调用,让内核来执行了, 此时你不清楚内核身上背负着很多的任务
        // 因此 我们无法确定 内核都要做哪些工作


        // **** 标准库中的线程池 ****
        //使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
        //返回值类型为 ExecutorService
        //通过 ExecutorService.submit 可以注册一个任务到线程池中
        // ThreadPoolExecutor 提供了更多的可选参数, 可以进一步细化线程池行为的设定.

        // ****ThreadPoolExecutor 这里的构造方法参数的解释是高频考试题****

        // 实现线程池
        //核心操作为 submit, 将任务加入线程池中
        //使用 Worker 类描述一个工作线程. 使用 Runnable 描述一个任务.
        //使用一个 BlockingQueue 组织所有的任务
        //每个 worker 线程要做的事情: 不停的从 BlockingQueue 中取任务并执行.
        //指定一下线程池中的最大线程数 maxWorkerCount; 当当前线程数超过这个最大值时, 就不再新增
        //线程了.

        //****10. 总结-保证线程安全的思路****
        // 1. 使用没有共享资源的模型
        //2. 适用共享资源只读，不写的模型
        //1. 不需要写共享资源的模型
        //2. 使用不可变对象
        //3. 直面线程安全（重点）
        //1. 保证原子性
        //2. 保证顺序性
        //3. 保证可见性


        //***** 11. 对比线程和进程 *****
        // 11.1 线程的优点
        //1. 创建一个新线程的代价要比创建一个新进程小得多
        //2. 与进程之间的切换相比，线程之间的切换需要操作系统做的工作要少很多
        //3. 线程占用的资源要比进程少很多
        //4. 能充分利用多处理器的可并行数量
        //5. 在等待慢速I/O操作结束的同时，程序可执行其他的计算任务
        //6. 计算密集型应用，为了能在多处理器系统上运行，将计算分解到多个线程中实现
        //7. I/O密集型应用，为了提高性能，将I/O操作重叠。线程可以同时等待不同的I/O操作。

        //11.2 进程与线程的区别
        // 1. 进程是系统进行资源分配和调度的一个独立单位，线程是程序执行的最小单位。
        //2. 进程有自己的内存地址空间，线程只独享指令流执行的必要资源，如寄存器和栈。
        //3. 由于同一进程的各线程间共享内存和文件资源，可以不通过内核进行直接通信。
        //4. 线程的创建、切换及终止效率更高。

    }
}